package com.dragon.common.utils.io.code;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.oned.Code128Reader;
import com.google.zxing.oned.Code128Writer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.Data;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
 * 条形码工具类
 *
 * @author dragon
 * @version 1.0
 * @date 2024/8/23 15:46
 */
public class BarCodeUtils {

    private static final Logger log = LoggerFactory.getLogger(BarCodeUtils.class);

    @Data
    public static class Config {
        /**
         * 条形码宽度，单位像素
         */
        private int barWidth = 300;

        /**
         * 条形码高度，单位像素
         */
        private int barHeight = 50;

        /**
         * 条形码前景色，0x000000 表示黑色
         */
        private Color frontColor = Color.BLACK;

        /**
         * 条形码背景色，0xFFFFFF 表示白色
         */
        private Color backgroundColor = Color.WHITE;

        /**
         * 纠错级别
         * ErrorCorrectionLevel：误差校正等级，L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
         * 不设置时，默认为 L 等级，等级不一样，生成的图案不同，但扫描的结果是一样的
         */
        private ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.M;

        /**
         * 设置边距，既条形码和背景之间的边距
         */
        private int margin = 1;

        /**
         * 显示文字
         */
        private int showWord = 0;

        /**
         * 文字内容
         */
        private String word;

        /**
         * 文字高度
         */
        private int wordHeight = 25;

        public Config() {
        }

        public Config(int barWidth, int barHeight) {
            this.barWidth = barWidth;
            this.barHeight = barHeight;
        }
    }

    /**
     * 生成条形码
     *
     * @param content    条形码内容
     * @param targetFile 条形码文件
     * @throws IOException 写入条形码文件失败
     */
    public static void generate(String content, File targetFile) throws IOException {
        BufferedImage bufferedImage = generate(new Config(), content);

        ImageIO.write(bufferedImage, FilenameUtils.getExtension(targetFile.getName()), targetFile);
    }

    /**
     * 生成条形码
     *
     * @param content      条形码内容
     * @param outputStream 条形码输出流
     * @throws IOException 写入条形码文件失败
     */
    public static void generate(String content, OutputStream outputStream) throws IOException {
        BufferedImage bufferedImage = generate(new Config(), content);

        ImageIO.write(bufferedImage, "png", outputStream);
    }

    /**
     * 生成条形码
     *
     * @param content    条形码内容
     * @param targetFile 条形码文件
     * @param barWidth   条形码宽度
     * @param barHeight  条形码高度
     * @throws IOException 写入条形码文件失败
     */
    public static void generate(String content, File targetFile, int barWidth, int barHeight) throws IOException {
        BufferedImage bufferedImage = generate(new Config(barWidth, barHeight), content);

        ImageIO.write(bufferedImage, FilenameUtils.getExtension(targetFile.getName()), targetFile);
    }

    /**
     * 生成条形码
     *
     * @param content      条形码内容
     * @param outputStream 条形码输出流
     * @param barWidth     条形码宽度
     * @param barHeight    条形码高度
     * @throws IOException 写入条形码文件失败
     */
    public static void generate(String content, OutputStream outputStream, int barWidth, int barHeight) throws IOException {
        BufferedImage bufferedImage = generate(new Config(barWidth, barHeight), content);

        ImageIO.write(bufferedImage, "png", outputStream);
    }

    /**
     * 生成条形码
     *
     * @param config     条形码配置
     * @param content    条形码内容
     * @param targetFile 条形码文件
     * @throws IOException 写入条形码文件失败
     */
    public static void generate(Config config, String content, File targetFile) throws IOException {
        BufferedImage bufferedImage = generate(config, content);

        ImageIO.write(bufferedImage, FilenameUtils.getExtension(targetFile.getName()), targetFile);
    }

    /**
     * 生成条形码
     *
     * @param config       条形码配置
     * @param content      条形码内容
     * @param outputStream 条形码输出流
     * @throws IOException 写入条形码文件失败
     */
    public static void generate(Config config, String content, OutputStream outputStream) throws IOException {
        BufferedImage bufferedImage = generate(config, content);

        ImageIO.write(bufferedImage, "png", outputStream);
    }

    /**
     * 生成条形码
     *
     * @param config  条形码配置
     * @param content 条形码内容
     * @return 条形码图片
     */
    public static BufferedImage generate(Config config, String content) {

        /*
         * com.google.zxing.EncodeHintType：编码提示类型,枚举类型
         * EncodeHintType.CHARACTER_SET：设置字符编码类型
         * EncodeHintType.ERROR_CORRECTION：设置误差校正
         *      ErrorCorrectionLevel：误差校正等级，L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
         *      不设置时，默认为 L 等级，等级不一样，生成的图案不同，但扫描的结果是一样的
         * EncodeHintType.MARGIN：设置条形码边距，单位像素，值越小，条形码距离四周越近
         */
        Map<EncodeHintType, Object> hints = new HashMap<>(3);
        hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);
        hints.put(EncodeHintType.ERROR_CORRECTION, config.getErrorCorrection());
        hints.put(EncodeHintType.MARGIN, config.getMargin());

        Code128Writer code128Writer = new Code128Writer();
        // 编码内容, 编码类型, 宽度, 高度, 设置参数
        BitMatrix bitMatrix = code128Writer.encode(content, BarcodeFormat.CODE_128, config.getBarWidth(), config.getBarHeight(), hints);

        /*
         * java.awt.image.BufferedImage：具有图像数据的可访问缓冲图像，实现了 RenderedImage 接口
         * BitMatrix 的 get(int x, int y) 获取比特矩阵内容，指定位置有值，则返回true，将其设置为前景色，否则设置为背景色
         * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
         *      x：像素位置的横坐标，即列
         *      y：像素位置的纵坐标，即行
         *      rgb：像素的值，采用 16 进制,如 0xFFFFFF 白色
         */
        BufferedImage bufferedImage = drawQrCode(config, bitMatrix);

        if (StringUtils.isBlank(config.getWord())) {
            config.setWord(content);
        }

        // 绘制文字
        bufferedImage = drawWord(config, bufferedImage);

        return bufferedImage;
    }

    /**
     * 绘制条形码
     *
     * @param config    条形码配置
     * @param bitMatrix 条形码矩阵
     * @return 条形码图片
     */
    private static @NonNull BufferedImage drawQrCode(Config config, BitMatrix bitMatrix) {
        BufferedImage bufferedImage = new BufferedImage(config.getBarWidth(), config.getBarHeight(), BufferedImage.TYPE_INT_BGR);
        for (int x = 0; x < config.getBarWidth(); x++) {
            for (int y = 0; y < config.getBarHeight(); y++) {
                bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? config.frontColor.getRGB() : config.getBackgroundColor().getRGB());
            }
        }

        // return MatrixToImageWriter.toBufferedImage(bitMatrix);
        return bufferedImage;
    }

    /**
     * 绘制文字
     *
     * @param config        条形码配置
     * @param bufferedImage 条形码图片
     * @return 条形码图片
     */
    private static BufferedImage drawWord(Config config, BufferedImage bufferedImage) {
        if (config.getShowWord() != 1) {
            return bufferedImage;
        }

        BufferedImage wordImage = new BufferedImage(config.getBarWidth(), config.getBarHeight() + config.getWordHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics2D = wordImage.createGraphics();
        // 设置背景色
        graphics2D.setColor(config.getBackgroundColor());
        //填充整个屏幕
        graphics2D.fillRect(0, 0, 600, 600);
        // 设置笔刷
        graphics2D.setColor(config.getFrontColor());
        // 画条形码到新的面板
        graphics2D.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), null);
        // 字体、字型、字号
        graphics2D.setFont(new Font("微软雅黑", Font.PLAIN, 18));
        // 文字长度
        int strWidth = graphics2D.getFontMetrics().stringWidth(config.getWord());
        // 总长度减去文字长度的一半  （居中显示）
        int wordStartX = (config.getBarWidth() - strWidth) / 2;
        int wordStartY = config.getBarHeight() + 20;

        // 画文字
        graphics2D.drawString(config.getWord(), wordStartX, wordStartY);
        graphics2D.dispose();
        wordImage.flush();

        return wordImage;
    }

    /**
     * 解析条形码
     *
     * @param file 条形码文件
     * @return 条形码内容
     */
    public static String decode(File file) {
        if (file == null || !file.exists()) {
            return null;
        }

        try {
            /*
             * ImageIO 的 BufferedImage read(URL input) 方法用于读取网络图片文件转为内存缓冲图像
             * 同理还有：read(File input)、read(InputStream input)、、read(ImageInputStream stream)
             */
            BufferedImage bufferedImage = ImageIO.read(file);

            return decode(bufferedImage);
        } catch (Exception e) {
            log.error("读取条形码失败 {}", e.getMessage(), e);
        }

        return null;
    }

    /**
     * 解析条形码
     *
     * @param url 条形码链接
     * @return 条形码内容
     */
    public static String decode(URL url) {
        if (url == null) {
            return null;
        }

        try {
            /*
             * ImageIO 的 BufferedImage read(URL input) 方法用于读取网络图片文件转为内存缓冲图像
             * 同理还有：read(File input)、read(InputStream input)、、read(ImageInputStream stream)
             */
            BufferedImage bufferedImage = ImageIO.read(url);

            return decode(bufferedImage);
        } catch (Exception e) {
            log.error("读取条形码失败 {}", e.getMessage(), e);
        }

        return null;
    }

    /**
     * 解析条形码
     *
     * @param bufferedImage 条形码图像
     * @return 条形码内容
     */
    public static String decode(BufferedImage bufferedImage) {
        if (bufferedImage == null) {
            return null;
        }

        try {
            /*
             * com.google.zxing.client.j2se.BufferedImageLuminanceSource：缓冲图像亮度源
             * 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源
             * 关键就是下面这几句：HybridBinarizer 用于读取条形码图像数据，BinaryBitmap 二进制位图
             */
            BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
            Hashtable<DecodeHintType, Object> hints = new Hashtable<>(1);
            hints.put(DecodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);

            /*
             * 如果图片不是条形码图片，则 decode 抛异常：com.google.zxing.NotFoundException
             * MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵
             * MultiFormatReader 的 decode 用于读取二进制位图数据
             */
            Result result = new Code128Reader().decode(bitmap, hints);
            return result.getText();
        } catch (Exception e) {
            log.error("读取条形码失败 {}", e.getMessage(), e);
        }

        return null;
    }

}
